/**
 * Kernel Monkeys
 * Andrew Boguk
 * Sergiy Kulyk
 * Nicholas Swartzendruber
 */

/*
 * cpu.c
 *
 *  Created on: Jun 7, 2011
 *      Author: Sergiy Kulyk
 *      Minor Edits: Nicholas Swartzendruber
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "cpu.h"
#include "process.h"

CPU cpu;

/**
 * Starts the program.
 */
int main() {
        cpu = cpu_new();
        cpu_run(cpu);
        return 0;
}

/**
 * PCB record constructor. All records are created in the "ready" state, with
 * the 0 for the next instruction to run, not waiting on any values, and not
 * owning any semaphores.
 *
 * @param pid Process ID.
 * @return A pointer to the PCB record.
 */
PCBRecord cpu_new_pcb(const int pid) {

        PCBRecord pcb = malloc(sizeof(pcb_t));

        pcb->pid = pid;
        pcb->next_step = 0;
        pcb->state = 1;
        pcb->waiting_on = 0;
        pcb->owns = 0;

        return pcb;
}


/**
 * Set the PCB record to "blocked" state, and register
 * the I/O device it is waiting on.
 *
 * @param pcb The PCB record.
 * @param waiting_on I/O device this process is waiting on.
 */
void cpu_pcb_set_waiting(PCBRecord pcb, const int waiting_on) {

        pcb->state = 3;
        pcb->waiting_on = waiting_on;
}


/**
 * Set the PCB record to "ready" state, and unregister
 * the I/O device it was waiting on (if any).
 *
 * @param pcb The PCB record.
 */
void cpu_pcb_set_ready(PCBRecord pcb) {

        pcb->state = 1;
        pcb->waiting_on = 0;
}




/****************************************************************************
 *                      C P U
 ****************************************************************************/


/**
 * Setup the ready queue with PCB records.
 */
Queue _cpu_setup_ready_queue(const int num_proc);

/**
 * Setup the block queues.
 */
void _cpu_setup_block_queues(const int num_proc, Queue block_q[]);

/**
 * Setup the processes queue.
 */
Queue _cpu_setup_processes(const int num_proc);


/**
 * Timer callback function.
 */
void _cpu_timer_callback(void);

/**
 * Keyboard int callback.
 */
void _cpu_key_int_handler(void);

/**
 * Disk I/O interrupt handler.
 */
void _cpu_disk_int_handler(void);

/**
 * Video I/O interrupt handler.
 */
void _cpu_video_int_handler(void);

/**
 * Key callback handler.
 */
void _cpu_key_call_handler(void);


process_t * _cpu_get_process(Queue q, PCBRecord pcb);



/**
 * CPU constructor. It creates the
 *
 */
CPU cpu_new(void) {

    // create the queue with PCB records
    Queue ready_queue = _cpu_setup_ready_queue(MAX_PROCESSES);

    // create the queue with processes
    Queue processes = _cpu_setup_processes(MAX_PROCESSES);

    CPU cpu = (CPU) malloc(sizeof(cpu_t));
    if (ready_queue && processes && cpu) {
        cpu->scheduler = sched_create(ready_queue);
        cpu->timer = timer_initialize(_cpu_timer_callback);
        cpu->processes = processes;
        cpu->io_num = NUM_IO;
        cpu->io[DISK_IO] = io_dev_initialize(_cpu_disk_int_handler, DISK);
        cpu->io[VIDEO_IO] = io_dev_initialize(_cpu_video_int_handler, VIDEO);
        cpu->io[KEY_IO] = io_dev_initialize(_cpu_key_call_handler, KEYBOARD);
        _cpu_setup_block_queues(NUM_IO, cpu->block_q);
    }

    return cpu;
}


/**
 * Run the CPU.
 *
 * @param cpu The CPU to run.
 */
void cpu_run(const CPU cpu) {
        //Start the timer
        timer_start(cpu->timer);

        //Start the keyboard listener (for key int.)
        keyboard_start(&_cpu_key_int_handler);
        printf("[cpu.c] Timer started.\n");
        printf("[cpu.c] Starting the CPU...\n");

    while (1) {
        int next_step = 0;
        enum request_type_t req = IO;   // dummy value
        enum request_type_t * request = &req;

        PCBRecord pcb = cpu->scheduler->running;

        process_t * p = _cpu_get_process(cpu->processes, pcb);

        next_step = process_run(p, next_step, request);

        //TODO debug
        printf("[cpu.c] Ran process # %d, next step is %d\n", p->id, next_step);

        // update the PCB record
        pcb->next_step = next_step;

        // place the process in a blocked queue.
        if (*request == IO) {
                int type_c = rand() % 2;
                int type = DISK_IO;
                if(type_c == 0){
                        type = VIDEO_IO;
                }

                sched_switch(cpu->scheduler, INT_IO_REQUEST);
                io_dev_start(cpu->io[type]);
        }//else{
                        //This is where we would call our semaphore lock in sched.c,
                        //But this is kind of buggy here, and the single semaphore may
                        //never be unlocked.
        //}


    }
}


/******************************************************************************
 *          C P U private functions
 ******************************************************************************/

/**
 * Setup the ready queue with PCB records.
 */
Queue _cpu_setup_ready_queue(const int num_proc) {

    Queue ready_queue = queue_CreateQueue(num_proc);
    if (ready_queue) {
        int i;
        for (i = 0; i < num_proc; i++) {
            queue_Enqueue(cpu_new_pcb(i), ready_queue);
        }
    }

    return ready_queue;
}


/**
 * Setup the block queues.
 */
void _cpu_setup_block_queues(const int num_proc, Queue block_q[]) {

    int i;
    for (i = 0; i < num_proc; i++) {
        block_q[i] = queue_CreateQueue(MAX_PROCESSES);
    }
}


/**
 * Allocate and setup an array of requests.
 */
void _cpu_setup_requests(request_t** req, int num_req, int max_steps) {

        //TODO
        //printf("[cpu.c _cpu_setup_requests] num_req = %d\n", num_req);

    int step = max_steps / (num_req + 1);
    int curr_step = step;
    int i;
    for (i = 0; i < num_req; i++) {
        req[i] = request_new(step, rand() % NUM_REQ_TYPE);
        curr_step += step;
    }
    //TODO
    //printf("[cpu.c _cpu_setup_requests] num_req = %d, generated %d requests, first one is at %d \n", num_req, i, req[0]->step);
}


/**
 * Setup the processes queue.
 */
Queue _cpu_setup_processes(const int num_proc) {

    unsigned int iseed = (unsigned int)time(NULL);
    srand (iseed);

    Queue q = queue_CreateQueue(num_proc);
    if (q) {
        int i;
        for (i = 0; i < num_proc; i++) {
            int num_req = rand() % MAX_REQUESTS;
            num_req = num_req > 0 ? num_req : num_req + 1;
            request_t** req = (request_t**) malloc(sizeof(request_t) * num_req);
            int max_steps = rand() % MAX_STEPS;
            _cpu_setup_requests(req, num_req, max_steps);

            process_t* p = process_new(i,
                                      max_steps,
                                      num_req,
                                      req,
                                      rand() % NUM_PROC_TYPES);
            //printf("[cpu.c _cpu_setup_processes] Created process: id = %d, steps = %d, num_req = %d, first i/o = %d, p_type = %d\n",
            //          i, p->num_steps, p->num_requests, req[0]->step, p->process_type);
            queue_Enqueue(p, q);
        }
    }

    return q;
}



/**
 * Timer callback function.
 */
void _cpu_timer_callback(void) {
        sched_switch(cpu->scheduler, INT_TIMER);
}

void _cpu_key_int_handler(void) {
        sched_switch(cpu->scheduler, INT_IO_REQUEST);
        _cpu_key_call_handler();
}

void _cpu_key_call_handler(void) {
        printf("[cpu.c] Key interrupt serviced.\n");
        sched_switch(cpu->scheduler, INT_IO);
}


/**
 * Disk I/O interrupt handler.
 */
void _cpu_disk_int_handler(void) {
        printf("[cpu.c] Disk interrupt serviced.\n");
        sched_switch(cpu->scheduler, INT_IO);
}



/**
 * Video I/O interrupt handler.
 */
void _cpu_video_int_handler(void) {
        printf("[cpu.c] Video interrupt serviced.\n");
        sched_switch(cpu->scheduler, INT_IO);
}


/**
 * Find and return a process by PCB record.
 */
process_t * _cpu_get_process(Queue q, PCBRecord pcb) {

        process_t * p = queue_Front(q);
        while (p->id != pcb->pid) {
                p = queue_FrontAndDequeue(q);
                queue_Enqueue(p, q);
                p = queue_Front(q);
        }

        return p;
}
